package cz.habarta.typescript.generator.parser; import cz.habarta.typescript.generator.JaxrsApplicationScanner; import cz.habarta.typescript.generator.Settings; import cz.habarta.typescript.generator.util.Parameter; import cz.habarta.typescript.generator.util.Predicate; import cz.habarta.typescript.generator.util.Utils; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.*; import javax.ws.rs.ApplicationPath; import javax.ws.rs.CookieParam; import javax.ws.rs.FormParam; import javax.ws.rs.HeaderParam; import javax.ws.rs.HttpMethod; import javax.ws.rs.MatrixParam; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Application; import javax.ws.rs.core.Context; import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; public class JaxrsApplicationParser { private final Settings settings; private final Predicate<String> isClassNameExcluded; private final Set<String> defaultExcludes; private final JaxrsApplicationModel model; public JaxrsApplicationParser(Settings settings) { this.settings = settings; this.isClassNameExcluded = settings.getExcludeFilter(); this.defaultExcludes = new LinkedHashSet<>(getDefaultExcludedClassNames()); this.model = new JaxrsApplicationModel(); } public JaxrsApplicationModel getModel() { return model; } public static class Result { public List<SourceType<Type>> discoveredTypes; public Result() { discoveredTypes = new ArrayList<>(); } public Result(List<SourceType<Type>> discoveredTypes) { this.discoveredTypes = discoveredTypes; } } public Result tryParse(SourceType<?> sourceType) { if (!(sourceType.type instanceof Class<?>)) { return null; } final Class<?> cls = (Class<?>) sourceType.type; // application if (Application.class.isAssignableFrom(cls)) { final ApplicationPath applicationPathAnnotation = cls.getAnnotation(ApplicationPath.class); if (applicationPathAnnotation != null) { model.setApplicationPath(applicationPathAnnotation.value()); } model.setApplicationName(cls.getSimpleName()); final List<SourceType<Type>> discoveredTypes = JaxrsApplicationScanner.scanJaxrsApplication(cls, isClassNameExcluded); return new Result(discoveredTypes); } // resource final Path path = cls.getAnnotation(Path.class); if (path != null) { System.out.println("Parsing JAX-RS resource: " + cls.getName()); final Result result = new Result(); parseResource(result, new ResourceContext(cls, path.value()), cls); return result; } return null; } private void parseResource(Result result, ResourceContext context, Class<?> resourceClass) { // subContext final Map<String, Type> pathParamTypes = new LinkedHashMap<>(); for (Field field : resourceClass.getDeclaredFields()) { final PathParam pathParamAnnotation = field.getAnnotation(PathParam.class); if (pathParamAnnotation != null) { pathParamTypes.put(pathParamAnnotation.value(), field.getType()); } } final ResourceContext subContext = context.subPathParamTypes(pathParamTypes); // parse resource methods final List<Method> methods = Arrays.asList(resourceClass.getMethods()); Collections.sort(methods, new Comparator<Method>() { @Override public int compare(Method o1, Method o2) { return o1.getName().compareToIgnoreCase(o2.getName()); } }); for (Method method : methods) { parseResourceMethod(result, subContext, resourceClass, method); } } private void parseResourceMethod(Result result, ResourceContext context, Class<?> resourceClass, Method method) { final Path pathAnnotation = method.getAnnotation(Path.class); // subContext context = context.subPath(pathAnnotation); final Map<String, Type> pathParamTypes = new LinkedHashMap<>(); final List<Parameter> methodParameters = Parameter.ofMethod(method); for (Parameter parameter : methodParameters) { final PathParam pathParamAnnotation = parameter.getAnnotation(PathParam.class); if (pathParamAnnotation != null) { pathParamTypes.put(pathParamAnnotation.value(), parameter.getParameterizedType()); } } context = context.subPathParamTypes(pathParamTypes); // JAX-RS specification - 3.3 Resource Methods final HttpMethod httpMethod = getHttpMethod(method); if (httpMethod != null) { // swagger final SwaggerOperation swaggerOperation = settings.ignoreSwaggerAnnotations ? new SwaggerOperation() : Swagger.parseSwaggerAnnotations(method); if (swaggerOperation.possibleResponses != null) { for (SwaggerResponse response : swaggerOperation.possibleResponses) { if (response.responseType != null) { foundType(result, response.responseType, resourceClass, method.getName()); } } } if (swaggerOperation.hidden) { return; } // path parameters final List<MethodParameterModel> pathParams = new ArrayList<>(); final PathTemplate pathTemplate = PathTemplate.parse(context.path); for (PathTemplate.Part part : pathTemplate.getParts()) { if (part instanceof PathTemplate.Parameter) { final PathTemplate.Parameter parameter = (PathTemplate.Parameter) part; final Type type = context.pathParamTypes.get(parameter.getName()); pathParams.add(new MethodParameterModel(parameter.getName(), type != null ? type : String.class)); } } // query parameters final List<MethodParameterModel> queryParams = new ArrayList<>(); final List<Parameter> params = Parameter.ofMethod(method); for (Parameter param : params) { final QueryParam queryParamAnnotation = param.getAnnotation(QueryParam.class); if (queryParamAnnotation != null) { queryParams.add(new MethodParameterModel(queryParamAnnotation.value(), param.getParameterizedType())); } } // JAX-RS specification - 3.3.2.1 Entity Parameters final MethodParameterModel entityParameter = getEntityParameter(method); if (entityParameter != null) { foundType(result, entityParameter.getType(), resourceClass, method.getName()); } // JAX-RS specification - 3.3.3 Return Type final Class<?> returnType = method.getReturnType(); final Type genericReturnType = method.getGenericReturnType(); final Type modelReturnType; if (returnType == void.class) { modelReturnType = returnType; } else if (returnType == Response.class) { if (swaggerOperation.responseType != null) { modelReturnType = swaggerOperation.responseType; foundType(result, modelReturnType, resourceClass, method.getName()); } else { modelReturnType = Object.class; } } else if (genericReturnType instanceof ParameterizedType && returnType == GenericEntity.class) { final ParameterizedType parameterizedReturnType = (ParameterizedType) genericReturnType; modelReturnType = parameterizedReturnType.getActualTypeArguments()[0]; foundType(result, modelReturnType, resourceClass, method.getName()); } else { modelReturnType = genericReturnType; foundType(result, modelReturnType, resourceClass, method.getName()); } // comments final List<String> comments = Swagger.getOperationComments(swaggerOperation); // create method model.getMethods().add(new JaxrsMethodModel(resourceClass, method.getName(), modelReturnType, context.rootResource, httpMethod.value(), context.path, pathParams, queryParams, entityParameter, comments)); } // JAX-RS specification - 3.4.1 Sub Resources if (pathAnnotation != null && httpMethod == null) { parseResource(result, context, method.getReturnType()); } } private void foundType(Result result, Type type, Class<?> usedInClass, String usedInMember) { if (!isExcluded(type)) { result.discoveredTypes.add(new SourceType<>(type, usedInClass, usedInMember)); } } private boolean isExcluded(Type type) { final Class<?> cls = Utils.getRawClassOrNull(type); if (cls == null) { return false; } if (isClassNameExcluded != null && isClassNameExcluded.test(cls.getName())) { return true; } if (defaultExcludes.contains(cls.getName())) { return true; } for (Class<?> standardEntityClass : getStandardEntityClasses()) { if (standardEntityClass.isAssignableFrom(cls)) { return true; } } return false; } private static HttpMethod getHttpMethod(Method method) { for (Annotation annotation : method.getAnnotations()) { final HttpMethod httpMethodAnnotation = annotation.annotationType().getAnnotation(HttpMethod.class); if (httpMethodAnnotation != null) { return httpMethodAnnotation; } } return null; } private static MethodParameterModel getEntityParameter(Method method) { final List<Parameter> parameters = Parameter.ofMethod(method); for (Parameter parameter : parameters) { if (!hasAnyAnnotation(parameter, Arrays.asList( MatrixParam.class, QueryParam.class, PathParam.class, CookieParam.class, HeaderParam.class, Context.class, FormParam.class ))) { return new MethodParameterModel(parameter.getName(), parameter.getParameterizedType()); } } return null; } private static boolean hasAnyAnnotation(Parameter parameter, List<Class<? extends Annotation>> annotationClasses) { for (Class<? extends Annotation> annotationClass : annotationClasses) { for (Annotation parameterAnnotation : parameter.getAnnotations()) { if (annotationClass.isInstance(parameterAnnotation)) { return true; } } } return false; } public static List<Class<?>> getStandardEntityClasses() { // JAX-RS specification - 4.2.4 Standard Entity Providers return Arrays.asList( byte[].class, java.lang.String.class, java.io.InputStream.class, java.io.Reader.class, java.io.File.class, javax.activation.DataSource.class, javax.xml.transform.Source.class, javax.xml.bind.JAXBElement.class, MultivaluedMap.class, StreamingOutput.class, java.lang.Boolean.class, java.lang.Character.class, java.lang.Number.class, long.class, int.class, short.class, byte.class, double.class, float.class, boolean.class, char.class); } private static List<String> getDefaultExcludedClassNames() { return Arrays.asList( "org.glassfish.jersey.media.multipart.FormDataBodyPart" ); } private static class ResourceContext { public final Class<?> rootResource; public final String path; public final Map<String, Type> pathParamTypes; public ResourceContext(Class<?> rootResource, String path) { this(rootResource, path, new LinkedHashMap<String, Type>()); } private ResourceContext(Class<?> rootResource, String path, Map<String, Type> pathParamTypes) { this.rootResource = rootResource; this.path = path; this.pathParamTypes = pathParamTypes; } ResourceContext subPath(Path pathAnnotation) { final String subPath = pathAnnotation != null ? pathAnnotation.value() : null; return new ResourceContext(rootResource, Utils.joinPath(path, subPath), pathParamTypes); } ResourceContext subPathParamTypes(Map<String, Type> subPathParamTypes) { final Map<String, Type> newPathParamTypes = new LinkedHashMap<>(); newPathParamTypes.putAll(pathParamTypes); if (subPathParamTypes != null) { newPathParamTypes.putAll(subPathParamTypes); } return new ResourceContext(rootResource, path, newPathParamTypes); } } }